package userInterface;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JPanel;


public class DrawingView extends JPanel implements UpdateableView
{
	public DrawingView(DrawingModel model)
	{
		this.setName("drawingView");
		
		_graphics = this.getGraphics();
		_model = model;
		_model.addViewListener(this);
		this.setBackground(new Color(255, 255, 255));
		this.setFocusable(true);
		this.requestFocus();
	}
	
	@Override
	public void paint(Graphics g) 
	{	
		_graphics = g;
		super.paint(g);
		Graphics2D g2d = (Graphics2D)g;
		for (int i = 0; i < _model.nDrawObjects(); i++)
		{
			DrawObject obj = _model.getDrawObject(i);
			g2d.setColor(obj.getColor());
			
			switch (obj.getShape())
			{
			case LINE:
			{
				Line line = (Line) obj;
				g2d.setStroke(new BasicStroke(line.getThickness()));
				g2d.drawLine(
					line.getStartPoint().X,
					line.getStartPoint().Y,
					line.getEndPoint().X,
					line.getEndPoint().Y);
			}
				break;
			case RECTANGLE:
			{
				Rectangle rect = (Rectangle) obj;
				if (rect.getThickness() != 0)
				{
					g2d.setStroke(new BasicStroke(rect.getThickness()));
					g2d.drawRect(
							rect.getTopLeft().X,
							rect.getTopLeft().Y,
							rect.getWidth(),
							rect.getHeight());
				}
				else
				{
					g2d.fillRect(
							rect.getTopLeft().X,
							rect.getTopLeft().Y,
							rect.getWidth(),
							rect.getHeight());
				}
			}
				break;
			case ELLIPSE:
			{
				Ellipse e = (Ellipse) obj;
				if (e.getThickness() != 0)
				{
					g2d.setStroke(new BasicStroke(e.getThickness()));
					g2d.drawOval(
							e.getTopLeft().X,
							e.getTopLeft().Y,
							e.getWidth(),
							e.getHeight());
				}
				else
				{
					g2d.fillOval(
							e.getTopLeft().X,
							e.getTopLeft().Y,
							e.getWidth(),
							e.getHeight());
				}
			}
				break;
			case CURVE:
			{
				Curve c = (Curve) obj;
				g2d.setStroke(new BasicStroke(c.getThickness()));
				g2d.draw(new CubicCurve2D.Float(
						c.getStart().X,
						c.getStart().Y,
						c.getCtl1().X,
						c.getCtl1().Y,
						c.getCtl2().X,
						c.getCtl2().Y,
						c.getEnd().X,
						c.getEnd().Y));
			}
				break;
			case TEXT:
			{
				Text t = (Text) obj;
				g2d.setFont(new Font("serif", Font.PLAIN, t.getSize()));
				g2d.drawString(
						t.getText(),
						t.getLocation().X,
						t.getLocation().Y);
			}
				break;
			}
		}
		
		SelectObject(g2d);
	}

	private void SelectObject(Graphics2D g2d)
	{
		if (_selectedObj == null || _selectedObj.getIndex() == -1)
			return;

		DrawObject obj = _model.getDrawObject(_selectedObj.getIndex());
		g2d.setColor(new Color(0, 0, 0));
		switch(obj.getShape())
		{
		case LINE:
		{
			Line l = (Line) obj;
			paintControlPoint(l.getStartPoint(), g2d);
			paintControlPoint(l.getEndPoint(), g2d);
		}
		break;
		case RECTANGLE:
		{
			Rectangle r = (Rectangle) obj;
			paintControlPoint(r.getTopLeft(), g2d);
			paintControlPoint(new Point(r.getTopLeft().X + r.getWidth(), r.getTopLeft().Y), g2d);
			paintControlPoint(new Point(r.getTopLeft().X, r.getTopLeft().Y + r.getHeight()), g2d);
			paintControlPoint(new Point(r.getTopLeft().X + r.getWidth(), r.getTopLeft().Y + r.getHeight()), g2d);
		}
		break;
		case ELLIPSE:
		{
			Ellipse e = (Ellipse) obj;
			paintControlPoint(e.getTopLeft(), g2d);
			paintControlPoint(new Point(e.getTopLeft().X, e.getTopLeft().Y + e.getHeight()), g2d);
			paintControlPoint(new Point(e.getTopLeft().X + e.getWidth(), e.getTopLeft().Y), g2d);
			paintControlPoint(new Point(e.getTopLeft().X + e.getWidth(), e.getTopLeft().Y + e.getHeight()), g2d);
		}
		break;	
		case CURVE:
		{
			Curve c = ((Curve)obj);
			Point start = c.getStart();
			Point end = c.getEnd();
			Point ctl1 = c.getCtl1();
			Point ctl2 = c.getCtl2();
			g2d.fillOval(start.X - 5, start.Y - 5, 10, 10);
			g2d.fillOval(end.X - 5, end.Y - 5, 10, 10);
			g2d.fillOval(ctl1.X - 5, ctl1.Y - 5, 10, 10);
			g2d.fillOval(ctl2.X - 5, ctl2.Y - 5, 10, 10);
		}
		break;
		case TEXT:
		{
			Text text = ((Text) obj);
			Point loc = text.getLocation();
			g2d.fillOval(loc.X - 5, loc.Y - 5, 10, 10);
			
			int textLocation = _selectedObj.getTextIndex();
			String str;
			str = text.getText().substring(0, textLocation);
			FontMetrics fm = g2d.getFontMetrics(new Font("serif", Font.PLAIN, text.getSize()));
			
			int xoffset = (int) fm.getStringBounds(str, g2d).getWidth(); 

			g2d.drawLine(loc.X + xoffset, loc.Y, loc.X + xoffset, loc.Y - ((Text) obj).getSize());
		}
		break;
		}
	}
	
	public void paintControlPoint(Point p, Graphics2D g2d)
	{
		g2d.fillOval(p.X - 5, p.Y - 5, 10, 10);		
	}
	
	public void setSelectedObject(GeometryDescriptor obj)
	{
		_selectedObj = obj;
	}
	
	public GeometryDescriptor getSelectedObject()
	{
		return _selectedObj;
	}
	
	public void clearSelectedObject()
	{
		_selectedObj = null;
	}
	
	/**
	 * This section is the ESSINTIAL GEOMETRY
	 * 
	 * 
	 * 
	 */
	
	public GeometryDescriptor pointGeometry(Point mouseLoc)
	{	
		this.requestFocus();
		
		// check for a click on a control point
		if (_selectedObj != null && _selectedObj.getIndex() != -1)
		{
			int cp = findControlPoint(_selectedObj, mouseLoc);
			if (cp != -1)
				return new GeometryDescriptor(_selectedObj.getIndex(), _selectedObj.getTextIndex(), cp);
		}
		
		// check for a click on an object
		for (int i = _model.nDrawObjects() - 1; i >= 0; i--)
		{
			DrawObject obj = _model.getDrawObject(i);
			
			switch (obj.getShape())
			{
			case LINE:
				if (hitsLine((Line) obj, mouseLoc))
					return new GeometryDescriptor(i, -1, -1);
				break;

			case RECTANGLE:
				if (hitsRectangle((Rectangle) obj, mouseLoc))
					return new GeometryDescriptor(i, -1);
				break;

			case ELLIPSE:
				if (hitsEllipse((Ellipse) obj, mouseLoc))
					return new GeometryDescriptor(i, -1);
				break;

			case CURVE:
				if (hitsCurve((Curve) obj, mouseLoc))
					return new GeometryDescriptor(i, -1);
				break;
				
			case TEXT:
				if (hitsText((Text) obj, mouseLoc))
					return new GeometryDescriptor(i, findCharIntersected((Text) obj, mouseLoc));
				break;
			}
		}
		
		_selectedObj = null;
		return new GeometryDescriptor(-1, -1);
	}	

	private boolean hitsLine(Line line, Point mouseLoc)
	{
		int maxX, minX, maxY, minY;
		if (line.getStartPoint().X > line.getEndPoint().X)
		{
			maxX = line.getStartPoint().X;
			minX = line.getEndPoint().X;
		}
		else
		{
			maxX =  line.getEndPoint().X;
			minX = line.getStartPoint().X;
		}
		
		if (line.getStartPoint().Y > line.getEndPoint().Y)
		{
			maxY = line.getStartPoint().Y;
			minY = line.getEndPoint().Y;
		}
		else
		{
			maxY = line.getEndPoint().Y;
			minY = line.getStartPoint().Y;
		}
		
		// check for bounding box
		if (mouseLoc.X < minX - 5 || mouseLoc.X > maxX + 5 || mouseLoc.Y < minY - 5 || mouseLoc.Y > maxY + 5)
			return false;
		
		// enter equation into normalized implicit form and find distance to mouse click
		double a = line.getStartPoint().Y - line.getEndPoint().Y;
		double b = line.getEndPoint().X - line.getStartPoint().X;
		double c = -line.getStartPoint().X * a - line.getStartPoint().Y * b;
	
		double normalizer = Math.sqrt(Math.pow(a, 2.0) + Math.pow(b, 2.0));
		
		// normalize
		a = a / normalizer;
		b = b / normalizer;
		c = c / normalizer;
		
		double distance = a * mouseLoc.X + b * mouseLoc.Y + c;
		
		if (Math.abs(distance) <= 5)
			return true;
		return false;
	}
	private boolean hitsRectangle(Rectangle rect, Point mouseLoc)
	{
		int tlx = rect.getTopLeft().X;
		int tly = rect.getTopLeft().Y;
		int x = mouseLoc.X;
		int y = mouseLoc.Y;
		int w = rect.getWidth();
		int h = rect.getHeight();
		
		if (rect.getThickness() == 0)
			return new Rectangle2D.Double(tlx, tly, w, h).contains(x, y);
		
		return new Rectangle2D.Double(tlx - 5, tly - 5, w + 10, h + 10).contains(x, y) &&
				!(new Rectangle2D.Double(tlx + 5, tly + 5, w - 10, h - 10).contains(x, y));
	}	
	private boolean hitsEllipse(Ellipse ellipse, Point mouseLoc)
	{
		if (ellipse.getThickness() == 0)
			return new Ellipse2D.Double(ellipse.getTopLeft().X, ellipse.getTopLeft().Y, ellipse.getWidth(), ellipse.getHeight()).contains(mouseLoc.X, mouseLoc.Y);
		return new Ellipse2D.Double(ellipse.getTopLeft().X - 5, ellipse.getTopLeft().Y - 5, ellipse.getWidth() + 10, ellipse.getHeight() + 10).contains(mouseLoc.X, mouseLoc.Y) && 
				!(new Ellipse2D.Double(ellipse.getTopLeft().X + 5, ellipse.getTopLeft().Y + 5, ellipse.getWidth() - 10, ellipse.getHeight() - 10).contains(mouseLoc.X, mouseLoc.Y));
	}
	private boolean hitsCurve(Curve curve, Point mouseLoc)
	{
		// TODO make this work
		Rectangle2D rect = new Rectangle2D.Double(mouseLoc.X - 5, mouseLoc.Y - 5, 10, 10);
		CubicCurve2D c = new CubicCurve2D.Double(curve.getStart().X, curve.getStart().Y, curve.getCtl1().X, curve.getCtl1().Y, curve.getCtl2().X, curve.getCtl2().Y, curve.getEnd().X, curve.getEnd().Y);
		return c.intersects(rect);
	}
	private boolean hitsText(Text text, Point mouseLoc)
	{
		
		Font font = new Font("serif", Font.PLAIN, text.getSize());
		Rectangle2D rect = font.getStringBounds(text.getText(), new FontRenderContext(font.getTransform(), true, true));
		rect.setFrame(rect.getX() + text.getLocation().X, rect.getY() + text.getLocation().Y, rect.getWidth(), rect.getHeight());
		_graphics.drawRect(
				(int)(rect.getX() + text.getLocation().X),
				(int)(rect.getY() + text.getLocation().Y),
				(int)rect.getWidth(),
				(int)rect.getHeight());
		
		return rect.contains(mouseLoc.X, mouseLoc.Y);
	}

	private int findCharIntersected(Text text, Point mouseLoc)
	{
		String str = text.getText();
		FontMetrics fm = _graphics.getFontMetrics(new Font("serif", Font.PLAIN, text.getSize()));
		Rectangle2D rect;
		for (int i = 1; i <= str.length(); i++)
		{
			rect = fm.getStringBounds(str.substring(0, i), _graphics);
			rect.setFrame(rect.getX() + text.getLocation().X, rect.getY() + text.getLocation().Y, rect.getWidth(), rect.getHeight());
			if (rect.contains(mouseLoc.X, mouseLoc.Y))
				return i - 1;
		}
		return text.getText().length();
	}	
	
	private int findControlPoint(GeometryDescriptor desc, Point mouseLoc)
	{
		if (desc.getIndex() == -1)
			return -1;
		
		DrawObject obj = _model.getDrawObject(desc.getIndex());
		switch (obj.getShape())
		{
		case LINE:
		{
			if (isInControl(((Line) obj).getStartPoint(), mouseLoc))
				return 0;
			if (isInControl(((Line) obj).getEndPoint(), mouseLoc))
				return 1;			
		}
		break;
		case RECTANGLE:
		{
			Rectangle r = (Rectangle) obj;
			if (isInControl(r.getTopLeft(), mouseLoc))
				return 0;
			if (isInControl(new Point(r.getTopLeft().X + r.getWidth(), r.getTopLeft().Y), mouseLoc))
				return 1;
			if (isInControl(new Point(r.getTopLeft().X, r.getTopLeft().Y + r.getHeight()), mouseLoc))
				return 2;
			if (isInControl(new Point(r.getTopLeft().X + r.getWidth(), r.getTopLeft().Y + r.getHeight()), mouseLoc))
				return 3;
		}
		break;
		case ELLIPSE:
		{
			Ellipse e = (Ellipse) obj;
			if (isInControl(e.getTopLeft(), mouseLoc))
				return 0;			
			if (isInControl(new Point(e.getTopLeft().X + e.getWidth(), e.getTopLeft().Y), mouseLoc))
				return 1;			
			if (isInControl(new Point(e.getTopLeft().X, e.getTopLeft().Y + e.getHeight()), mouseLoc))
				return 2;			
			if (isInControl(new Point(e.getTopLeft().X + e.getWidth(), e.getTopLeft().Y + e.getHeight()), mouseLoc))
				return 3;			
		}
		break;
		case CURVE:
		{
			if (isInControl(((Curve) obj).getStart(), mouseLoc))
				return 0;
			else if (isInControl(((Curve) obj).getCtl1(), mouseLoc))
				return 1;
			else if (isInControl(((Curve) obj).getCtl2(), mouseLoc))
				return 2;
			else if (isInControl(((Curve) obj).getEnd(), mouseLoc))
				return 3;			
		}
		break;
		case TEXT:
		{
			if (isInControl(((Text) obj).getLocation(), mouseLoc))
				return 0;			
		}
		break;
		}
		
		return -1;
	}
	private boolean isInControl(Point ctl, Point p)
	{
		return new Ellipse2D.Double(ctl.X - 5, ctl.Y - 5, 10, 10).contains(p.X, p.Y);
	}
	
	private Graphics _graphics;
	private DrawingModel _model;
	
	// this is the currently selected object
	private GeometryDescriptor _selectedObj;
	
	private static final long serialVersionUID = 1L;

	@Override
	public void update()
	{
		this.repaint();
	}

}
